/*
 * Copyright 2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.seppiko.chart.utils

import com.google.zxing.*
import com.google.zxing.client.j2se.BufferedImageLuminanceSource
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.multi.GenericMultipleBarcodeReader
import com.google.zxing.multi.MultipleBarcodeReader
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.IOException
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.util.*
import javax.imageio.ImageIO

/**
 *
 * @author Leonard Woo
 */
class ImageUtil {
  companion object {
    private val logger = LoggingManager.INSTANCE.getLogger(ImageUtil::class.qualifiedName!!)

    const val MAX_PIXELS: Int = 1 shl 25
    fun processStream(input: InputStream): BufferedImage? {
      val image: BufferedImage?
      try {
        image = ImageIO.read(input)
      } catch (e: IOException) {
        logger.info(e.toString())
        return null
      } catch (e: IllegalArgumentException) {
        logger.info(e.toString())
        return null
      } catch (e: ArrayIndexOutOfBoundsException) {
        logger.info(e.toString())
        return null
      }
      if (image == null) {
        return null
      }

      val height = image.height
      val width = image.width
      if (height <= 1 || width <= 1 || (height * width) > MAX_PIXELS) {
        logger.info("Dimensions out of bounds: " + width + 'x' + height)
        return null
      }
      return image
    }

    private var HINTS: MutableMap<DecodeHintType, Any?>? = null
    private var HINTS_PURE: MutableMap<DecodeHintType, Any?>? = null
    init {
      HINTS = EnumMap(DecodeHintType::class.java)
      HINTS!![DecodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name()
      HINTS!![DecodeHintType.TRY_HARDER] = true
      HINTS!![DecodeHintType.POSSIBLE_FORMATS] = EnumSet.allOf(BarcodeFormat::class.java)

      HINTS_PURE = EnumMap(HINTS)
      HINTS_PURE!![DecodeHintType.PURE_BARCODE] = true
    }

    fun processImage(image: BufferedImage): ArrayList<Result> {
      val source: LuminanceSource = BufferedImageLuminanceSource(image)
      val bitmap = BinaryBitmap(HybridBinarizer(source))
      val results = ArrayList<Result>(1)
      try {
        val reader = MultiFormatReader()
        var savedException: ReaderException? = null
        try {
          // Look for multiple barcodes
          val multiReader: MultipleBarcodeReader = GenericMultipleBarcodeReader(reader)
          val theResults = multiReader.decodeMultiple(bitmap, HINTS)
          if (theResults != null) {
            results.addAll(Arrays.asList(*theResults))
          }
          logger.info("Results：$results")
        } catch (re: ReaderException) {
          savedException = re
        }
        if (results.isEmpty()) {
          try {
            // Look for pure barcode
            val theResult = reader.decode(bitmap, HINTS_PURE)
            if (theResult != null) {
              results.add(theResult)
            }
          } catch (re: ReaderException) {
            savedException = re
          }
        }
        if (results.isEmpty()) {
          try {
            // Look for normal barcode in photo
            val theResult = reader.decode(bitmap, HINTS)
            if (theResult != null) {
              results.add(theResult)
            }
          } catch (re: ReaderException) {
            savedException = re
          }
        }
        if (results.isEmpty()) {
          try {
            // Look for normal barcode in photo
            val theResult = reader.decodeWithState(bitmap)
            if (theResult != null) {
              results.add(theResult)
            }
          } catch (re: ReaderException) {
            savedException = re
          }
        }
        if (results.isEmpty()) {
          logger.warn("Bar code not found.", savedException)
        }
      } catch (re: RuntimeException) {
        // Call out unexpected errors in the log clearly
        logger.warn("Unexpected exception from library", re)
      }
      return results
    }

    fun addTextOnBottom(image: BufferedImage, content: String, margin: Int): BufferedImage {
      return addContent(image, Font("Arial", Font.PLAIN, 18), Color.BLACK, content, margin)
    }

    fun addContent(src: BufferedImage, font: Font, color: Color, content: String, margin: Int): BufferedImage {
      val height = src.height
      val width = src.width
      val dpi = 1
      val out = BufferedImage(width + margin * 2, height + font.size * dpi + margin * 2, src.type)
      val g = out.createGraphics()
      g.color = Color.WHITE
      g.fillRect(0, 0, out.width, out.height)
      g.drawImage(src, margin, margin, width, height, null)
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
      g.color = color
      g.font = font
      var fm = g.getFontMetrics(font)
      var textWidth = fm.stringWidth(content)
      val fontObj = Font(font.family, font.style, font.size - 1)
      while (textWidth > width) {
        g.font = fontObj
        fm = g.getFontMetrics(fontObj)
        textWidth = fm.stringWidth(content)
      }

      // vertical: bottom; align: center;
      val x = margin + (width - textWidth) / 2
      val y = margin + height + font.size
      g.drawString(content, x, y)
      g.dispose()
      return out
    }
  }
}